package oop.ex2.main;

import java.util.ArrayList;
import java.util.regex.*;

public class Method extends Patterns {

    // the object's fields
    private ArrayList<Variable> localVariables = new ArrayList<Variable>();
    private ArrayList<Variable> arguments = new ArrayList<Variable>();
    private ArrayList<Method> mainMethods = new ArrayList<Method>();
    private ArrayList<Variable> mainVariables = new ArrayList<Variable>();
    private String returnType;
    private String name;
    private Method superMethod;

    /**
     * Constructs a new Method object, while checking for valid input
     * 
     * @param line
     *            - the declaration line
     * @param mainVariables
     * @param superMethod
     * @throws compileException
     * @throws MethodException
     * @throws VariableException
     */
    public Method(String line, ArrayList<Method> mainMethods,
            ArrayList<Variable> mainVariables, Variable variableToAssign,
            Method superMethod) throws compileException, VariableException,
            MethodException {
        super(line);
        this.superMethod = superMethod;
        this.mainMethods = mainMethods;
        this.mainVariables = mainVariables;
        Pattern pattern = Pattern.compile(METHOD_DECLARATION);
        Matcher matcher = pattern.matcher(line);
        Pattern callMethod = Pattern.compile(METHOD_CALL);
        Matcher callMatcher = callMethod.matcher(line);
        if (matcher.matches()) {
            // parses the arguments from the method declaration
            this.returnType = matcher.group(1);
            this.name = matcher.group(2);
            if (!matcher.group(3).equals("")) {
                String[] cutVariables = matcher.group(3).split(",");
                for (String variable : cutVariables) {
                    Variable newVariable = new Variable(variable.trim() + ";",
                            mainVariables, mainMethods, null);
                    if (!newVariable.exists(arguments)) {
                        arguments.add(newVariable);
                    }
                    else {
                        throw new compileException("argument variable: <"
                                + newVariable.getName() + "> allready exists");
                    }
                }
            }
            System.out.println("match! name: " + name + ", returns: "
                    + returnType + ", args: " + matcher.group(3));
        }
        // for a case of a call for method
        else if (callMatcher.matches()) {
            // sets the boolean to match the call, false by default
            boolean callOK = false;
            // add the values to the method's object
            this.name = callMatcher.group(1);
            this.returnType = getReturn(this, mainMethods);
            parseCall(callMatcher, callOK, variableToAssign);
        }
        else {
            throw new compileException("The line: " + line
                    + ", mismatch to method declaration");
        }
    }

    /**
     * Parses a call for a method, for cases which there is a match for a method
     * call
     * 
     * @param callMatcher
     *            - the matcher that matches the call
     * @param callOK
     *            - the boolean to check
     * @throws compileException
     * @throws MethodException
     */
    private void parseCall(Matcher callMatcher, boolean callOK,
            Variable variable) throws compileException, MethodException {
        Pattern pattern = Pattern.compile(METHOD_CALL);
        ArrayList<String> cutVariables = parseArguments(callMatcher.group(2));
        for (String argument : cutVariables) {
            Matcher matcher = pattern.matcher(argument + ";");
            if (matcher.matches()) {
                parseCall(matcher, callOK, null);
            }
        }
        for (Method method : mainMethods) {
            // find if the method is declared by name
            if (callMatcher.group(1).trim().equals(method.getName())) {
                if (method.getVariables().size() != 0) {
                    for (int i = 0; i < cutVariables.size(); i++) {
                        VariableType type = VariableType.valueOf(method
                                .getVariables().get(i).getType().toUpperCase()
                                .trim());
                        String regex = type.getRegex();
                        // boolean for saving if the variables match
                        callOK = method.getVariables().get(i)
                                .matches(regex, cutVariables.get(i).trim());
                        // stop if the variables mismatch
                        if (!callOK) {
                            Matcher matcher = pattern.matcher(cutVariables
                                    .get(i) + ";");
                            if (matcher.matches()) {
                                for (Method returnMethod : mainMethods) {
                                    if (returnMethod.getName().equals(
                                            matcher.group(1))) {
                                        if (returnMethod.getReturnType()
                                                .equals(method.getVariables()
                                                        .get(i).getType())) {
                                            callOK = true;
                                        }
                                    }
                                }
                            }
                            else {
                                for (Variable argument : mainVariables) {
                                    if (argument.getName().equals(
                                            cutVariables.get(i))) {
                                        if (argument.getType().equals(
                                                method.getVariables().get(i)
                                                        .getType())) {
                                            callOK = true;
                                        }
                                    }
                                }
                                if (superMethod != null) {
                                    for (Variable argument : superMethod
                                            .getVariables()) {
                                        if (argument.getName().equals(
                                                cutVariables.get(i).trim())) {
                                            if (argument.getType().equals(
                                                    method.getVariables()
                                                            .get(i).getType())) {
                                                callOK = true;
                                            }
                                        }
                                    }
                                    for (Variable argument : superMethod
                                            .getLocalVariables()) {
                                        if (argument.getName().equals(
                                                cutVariables.get(i))) {
                                            if (argument.getType().equals(
                                                    method.getVariables()
                                                            .get(i).getType())) {
                                                callOK = true;
                                            }
                                        }
                                    }
                                }
                            }

                        }
                    }
                }
                else {
                    if (variable == null) {
                        callOK = true;

                    }
                }
            }
        }
        if (variable != null) {
            if (variable.getType().equals(returnType)) {

                callOK = true;
            }

        }
        if (callOK) {
            System.out.println("<" + callMatcher.group(1)
                    + ">: match method call");
        }
        // throw exception in case of a mismatch call
        else
            throw new compileException(
                    "method call mismatch to method declaration");

    }

    /**
     * Parses an argument in a method call. This method can recursively parse an
     * inner call for a method inside a call.
     * 
     * @param group
     *            - the call (or nested calls) for a method
     * @return the arguments divided
     */
    private ArrayList<String> parseArguments(String group) {
        ArrayList<String> arguments = new ArrayList<String>();
        int brackets = 0;
        int splitIndex = 0;
        for (int i = 0; i < group.length(); i++) {
            if (group.charAt(i) == '(') {
                brackets++;
            }
            else if (group.charAt(i) == ')') {
                brackets--;
            }
            if (group.charAt(i) == ',' && brackets == 0) {
                arguments.add(group.substring(splitIndex, i));
                splitIndex = i + 1;
            }
        }
        arguments.add(group.substring(splitIndex));

        return arguments;
    }

    /**
     * Gets the return type of the method
     * 
     * @param method
     *            - the method to collect the return type from
     * @param checkMethods
     *            - the ArrayList of methods to check
     * @return
     */
    private String getReturn(Method method, ArrayList<Method> checkMethods) {
        // scans the array and finds by name
        for (Method checkMethod : checkMethods) {
            if (method.getName().equals(checkMethod.getName())) {

                return checkMethod.getReturnType();
            }
        }
        return null;
    }

    /**
     * Checks a line in the method for declarations, assignments etc'
     * 
     * @param line
     *            - the line in the method
     * @throws compileException
     * @throws VariableException
     * @throws MethodException
     */
    public void checkMethod(String line) throws compileException,
            VariableException, MethodException {
        // for a variable declaration
        if (line.trim().matches(VARIABLE_DECLARATION)
                || line.trim().matches(VARIABLE_SHORT_DECLARATION)) {
            Variable variable = new Variable(line.trim(), mainVariables,
                    mainMethods, this);
            if (variable.exists(arguments) || variable.exists(localVariables)) {
                throw new compileException("variable <" + variable.getName()
                        + "> exists in the method arguments/local variables");
            }
            localVariables.add(variable);
        }
        // for a variable assignment
        else if (line.matches(VARIABLE_ASSAINMENT)) {
            Sjavac.updateVariable(mainVariables, arguments, line);
        }
        // for a return statement
        else if (line.matches(RETURN_STATEMENT)) {
            matchReturn(line);
        }
        else {
            if (!line.contains("}") && !line.contains("{")) {
                throw new MethodException("unknown line: <" + line + ">");
            }
        }

    }

    /**
     * Gets the name of the method
     * 
     * @return the name of the method
     */
    public String getName() {
        return this.name;
    }

    /**
     * Gets the method's arguments
     * 
     * @return an Arraylist of the methods arguments
     */
    public ArrayList<Variable> getVariables() {
        return this.arguments;
    }

    /**
     * Gets the methods local variables
     * 
     * @return an ArrayList of the method's local variables
     */
    public ArrayList<Variable> getLocalVariables() {
        return this.localVariables;
    }

    /**
     * Gets the return type of this method
     * 
     * @return the return type of the method
     */
    public String getReturnType() {
        return this.returnType;
    }

    /**
     * Checks if an ArrayList of methods contains the specified method
     * 
     * @param methods
     *            - the method to find
     * @return true if the array contains the method
     */
    public boolean contains(ArrayList<Method> methods) {
        boolean exists = false;
        for (Method method : methods) {
            if (this.getName().equals(method.getName())) {
                exists = true;
                break;
            }
        }
        return exists;
    }

    /**
     * Matches a line with a return statement in one of the main methods
     * 
     * @param line
     *            - The line to check
     * @throws compileException
     */
    public void matchReturn(String line) throws compileException {
        // create a pattern and a matcher
        Pattern pattern = Pattern.compile(RETURN_STATEMENT);
        Matcher matcher = pattern.matcher(line);
        VariableType type = VariableType.valueOf(this.getReturnType().trim()
                .toUpperCase());
        matcher.matches();
        // vor a void value
        if (this.returnType.equals("void") && matcher.group(2).matches("")) {
            System.out.println("void return matched!");
        }
        // match a regex with the return type
        else if (matcher.group(2).matches(type.getRegex())) {
            System.out.println(matcher.group(2) + ": return type matches! "
                    + this.getReturnType());
        }
        // match a variable name
        else if (matcher.group(2).matches(VARIABLE_NAME)) {
            boolean matches = false;
            // in the main variables
            for (Variable variable : mainVariables) {
                if (variable.getName().trim().equals(matcher.group(2).trim())
                        && (variable.getValue() != null)) {
                    System.out.println("match return: <" + matcher.group(2)
                            + "><" + variable.getName() + ">");
                    matches = true;
                }
            }
            // in the method's arguments
            for (Variable argument : arguments) {
                if (argument.getName().trim().equals(matcher.group(2).trim())) {
                    matches = true;
                }
            }
            // in the local variables
            for (Variable local : localVariables) {
                if (local.getName().trim().equals(matcher.group(2).trim())) {
                    matches = true;
                }
            }
            // throw exception in case of an error
            if (!matches) {
                throw new compileException("return variable not exist");
            }
        }

        else {
            throw new compileException("return mismatch");
        }
    }

}
